
/*
    Graph3DDoc.m

    The Graph3DDoc represents an open 3D Graph document.  Graph3DDoc receives
    messages from the user interface objects and uses three Expression objects
    to do calculations, and a RotatorCamera and a PointMesh to display the
    results.

    The Graph3DDoc class has one slightly odd external dependency.  It requires
    that the delegate of NXApp be able to provide it with a NXStringTable
    (via the stringTable method) that it can use to look up strings that
    are presented to the user.  In this application, the delegate of NXApp is
    always an instance of the GraphApp class.

    If this were a larger program, the NXStringTable needed by this document
    class would probably be in its own nib section, which would be loaded once
    the first time a Graph3DDoc is created, and then shared.

    You may freely copy, distribute, and reuse the code in this example.
    NeXT disclaims any warranty of any kind, expressed or implied, as to its
    fitness for any particular use.
*/

#import "Graph.h"

/* declare methods static to this class */
@interface Graph3DDoc(Private)
- _updateShape;
- _updateGraph:(BOOL)finalChange;
- (void)_equationChanged;
- (BOOL)_parseEquationFrom:(const char *)str using:(Expression *)expr enableVariables:(BOOL *)enableVars;
- _uOrVRangeSliderChanged:sender varName:(int)var;
- _uOrVRangeTextChanged:sender varName:(int)var;
- _write:(const char *)filename;
- _read:(const char *)filename;
- (void)_initVariable:(int)index enable:(BOOL)flag value:(float)val;
- _doSaveWithNewName:(BOOL)doNewName retainName:(BOOL)doRetain;
- _writeGraphToPasteboard:(Pasteboard *)pb types:(const char *const *)types num:(int)numTypes;
- (void)_setUVRangeOf:(int)var;
- (void)_setVariable:(int)index toValue:(float)val;
- (void)_setUpLighting;
- (void)_setSliderCell:(SliderCell *)slider value:(float)val minLabel:(Cell *)minLabel maxLabel:(Cell *)maxLabel;

@end

@implementation Graph3DDoc

/* variable names must be between A and H */
#define  MIN_CONST	'A'
#define  MAX_CONST	'H'

/* indices into instance variable arrays */
#define  X	0
#define  Y	1
#define  Z	2
#define  U	0
#define  V	1

/* default resolution of a new graph */
#define DEFAULT_RES	12

/* name used for the real document inside a doc wrapper */
#define DOC_NAME "/Graph3DDoc.xyzgraph"

static void writeCellFloat(Cell *obj, NXTypedStream *ts);
static NXTypedStream *openDocStream(const char *filename, int mode);
static int removeFile(const char *file);

- init {
  /* the default initialization is to just open a new document */
    return [self initFromFile:NULL];
}

/*
 * Opens a document.  If file is NULL, we open an untitiled document.  We also
 * create a NXDataLinkManager and hook ourselves up as its delegate for doing
 * Object Links.
 */
- initFromFile:(const char *)file {
    int rows, cols;
    int i;
    TextField *aCell;
    int xyz;
    RtPoint axis;
    char realPath[MAXPATHLEN+1];

    [super init];

  /* load the UI from the nib section and set attributes not available in IB */
    [NXApp loadNibSection:"Graph3DDoc.nib" owner:self withNames:NO fromZone:[self zone]];
    [variableTexts getNumRows:&rows numCols:&cols];
    for (i = rows; i--; ) {
	aCell = [variableTexts cellAt:i :0];
	[aCell setFloatingPointFormat:YES left:3 right:2];
	[aCell setEnabled:NO];
    }
    [minUVText[U] setFloatingPointFormat:YES left:3 right:2];
    [maxUVText[U] setFloatingPointFormat:YES left:3 right:2];
    [minUVText[V] setFloatingPointFormat:YES left:3 right:2];
    [maxUVText[V] setFloatingPointFormat:YES left:3 right:2];
    [variableSliders setAutosizeCells:YES];
    [resolutionText setEntryType:NX_POSINTTYPE];
    [camera setFieldOfViewByAngle:90.0];
    [self _setUpLighting];

  /* create Expression objects we will use to evaluate expressions */
    for (xyz = X; xyz <= Z; xyz++) {
	expr[xyz] = [[Expression allocFromZone:[self zone]] init];
	[expr[xyz] setDimensions:2];	/* we eval u * v points */
    }

  /* create and position the shape that we will use to show our data */
    [[camera setWorldShape:[[PointMesh alloc] init]] free];
    axis[1] = axis[2] = 0;
    axis[0] = 1.0;
    [[camera worldShape] rotateAngle:-90.0 axis:axis];

    if (file) {
      /* read existing document */
	if ([self _read:file]) {
	    name = NXCopyStringBufferFromZone(file, [self zone]);
	    if (realpath(name, realPath))
		realName = NXCopyStringBufferFromZone(realPath, [self zone]);
	    [window setTitleAsFilename:name];
	    [self _updateShape];
	    linkMgr = [[NXDataLinkManager allocFromZone:[self zone]] initWithDelegate:self fromFile:file];
	} else {
	    [self free];	/* couldn't load file */
	    return nil;
	}
    } else {
      /*
       * Create a new document.  We initialize it with a trivial expression
       * because that was easier than allowing the state where there is no
       * current expression.
       */
	RtPoint from = {12.0, 8.0, 10.0}, to = {0.0, 0.0, 0.0};

	[camera setEyeAt:from toward:to roll:0.0];
      /* set initial shape color to non-white on a color display */
	if ([camera shouldDrawColor])
	    [[camera worldShape] setColor:NXConvertRGBToColor(51.0/255.0, 153.0/255.0, 116.0/255.0)];

	[window setTitleAsFilename:[[[NXApp delegate] stringTable] valueForStringKey:"untitled doc"]];
	[resolutionSlider setIntValue:DEFAULT_RES];
	[resolutionText setIntValue:DEFAULT_RES];
	[equation[X] setStringValue:"u"];
	[equation[Y] setStringValue:"v"];
	[equation[Z] setStringValue:"A * (u^2 + v^2) + C"];
	for (xyz = X; xyz <= Z; xyz++) {
	    [expr[xyz] setResolution:DEFAULT_RES];
	}
	[self _equationChanged];
	[self _setUVRangeOf:U];
	[self _setUVRangeOf:V];
	[self _updateShape];
	linkMgr = [[NXDataLinkManager allocFromZone:[self zone]] initWithDelegate:self];
    }

    [[NXApp delegate] showThreeDPanel:self];
    [window makeKeyAndOrderFront:self];
    return self;
}

- free {
    int xyz;

    for (xyz = X; xyz <= Z; xyz++)
	[expr[xyz] free];
    NXZoneFree([self zone], name);
    NXZoneFree([self zone], realName);
    [linkMgr free];
    return [super free];
}

- (const char *)filename {
    return name;
}

- (const char *)realFilename {
    return realName;
}

/*
 * This method is called whenever our window becomes the app's main window.
 * When this happens we get the 3D Panel and set its camera to our camera, so
 * the panel reflects the attributes of our window.
 */
- windowDidBecomeMain:sender {
    [[[NXApp delegate] threeDPanel] setCamera:camera];
    return self;
}

#define BUF_MAX		256

/*
 * This method is called when the user has just finished typing in an
 * expression.  This is where we validate that it is a legal expression.
 * If we dont like the expression, we return YES, which tells the text object
 * not to accept the user's entry.
 */
- (BOOL)textWillEnd:textObject {
    BOOL parseError;
    BOOL enableVars[MAX_CONST - MIN_CONST + 1];
    NXStringTable *stringTable;
    Expression *tempExpr = [[Expression allocFromZone:[self zone]] init];
    char buffer[BUF_MAX];
    char *newText;
    int length;

  /* get the text out of the text object where the expression was typed */
    length = [textObject byteLength] + 1;
    if (length > BUF_MAX)
	newText = NXZoneMalloc(NXDefaultMallocZone(), sizeof(char) * length);
    else
	newText = buffer;
    [textObject getSubstring:newText start:0 length:length];

    if (*newText) {
	parseError = [self _parseEquationFrom:newText using:tempExpr enableVariables:enableVars];
	if (!parseError) {
	  /* parse the new equation into the real expressions */
	    [self _equationChanged];
    
	  /* update the shape object with current results and display it */
	    [self _updateGraph:YES];
	}
    } else
	parseError = YES;

    if (parseError) {
	stringTable = [[NXApp delegate] stringTable];
	NXRunAlertPanel([stringTable valueForStringKey:"parse alert title"],
			[stringTable valueForStringKey:"3D parse alert message"],
			[stringTable valueForStringKey:"ok button"],
			NULL, NULL);
    }
    [tempExpr free];
    if (length > BUF_MAX)
	NXZoneFree(NXDefaultMallocZone(), newText);
    return parseError;
}

/* This method does most of the work of parsing a new expression. */
- (void)_equationChanged {
    BOOL enableVars[MAX_CONST - MIN_CONST + 1];
    int i;
    int xyz;
    float newVarVal;

    for (i = 0; i <= MAX_CONST - MIN_CONST; i++)
	enableVars[i] = NO;
    for (xyz = X; xyz <= Z; xyz++) {
	(void)[self _parseEquationFrom:[equation[xyz] stringValue] using:expr[xyz] enableVariables:enableVars];
    }

  /* turn on the UI for constants used in these expressions */
    for (i = 0; i <= MAX_CONST - MIN_CONST; i++) {
	if ([[variableSliders cellAt:i :0] isEnabled])
	    newVarVal = [[variableSliders cellAt:i :0] floatValue];
	else
	    newVarVal = 1.0;
	[self _initVariable:i enable:enableVars[i] value:newVarVal];
    }

  /* update the range of the independent variables */
    [self _setUVRangeOf:U];
    [self _setUVRangeOf:V];
}

/*
 * This method is called after the user typed in a new expression.  The
 * expression has already been verified in our textWillEnd: method.
 */
- equationChanged:sender {
  /* reselect the equation field so the user can type another */
    [sender selectCell:[sender selectedCell]];
    return self;
}

/* called when either the minu, maxu, minv or maxv slider changes */
- _uOrVRangeSliderChanged:sender varName:(int)var {
    TextFieldCell *text;
    SliderCell *slider;
    NXEvent *event;

  /* update the associated text field */
    slider = [sender selectedCell];
    if (slider == maxUVSlider[var])
	text = maxUVText[var];
    else if (slider == minUVSlider[var])
	text = minUVText[var];
    else
	text = nil;
    NX_ASSERT(text, "Funny sender of _uOrVRangeSliderChanged: message");
    [text setFloatValue:[slider floatValue]];

  /* update the u or v range in the Expression and display the new graph */
    [self _setUVRangeOf:var];
    event = [NXApp currentEvent];
    [self _updateGraph:event->type == NX_LMOUSEUP];
    return self;
}

/* called when either the minu or maxu slider changes */
- uRangeSliderChanged:sender {
    return [self _uOrVRangeSliderChanged:sender varName:U];
}

/* called when either the minv or maxv slider changes */
- vRangeSliderChanged:sender {
    return [self _uOrVRangeSliderChanged:sender varName:V];
}

/* called when either the minu, maxu, minv or maxv text changes */
- _uOrVRangeTextChanged:sender varName:(int)var {
    TextFieldCell *text;
    SliderCell *slider;
    float val;

  /*
   * update the associated slider.  If the value typed is outside the current
   * range of the slider, we extend its range.
   */
    text = [sender selectedCell];
    if (text == maxUVText[var])
	slider = maxUVSlider[var];
    else if (text == minUVText[var])
	slider = minUVSlider[var];
    else
	slider = nil;
    NX_ASSERT(slider, "Funny sender of _uOrVRangeTextChanged: message");
    val = [text floatValue];
    [self _setSliderCell:slider value:val minLabel:nil maxLabel:nil];

  /* update the u or v range in the Expression and display the new graph */
    [self _setUVRangeOf:var];
    [self _updateGraph:YES];
    [sender selectText:self];
    return self;
}

/* called when either the minu or maxu text changes */
- uRangeTextChanged:sender {
    return [self _uOrVRangeTextChanged:sender varName:U];
}

/* called when either the minv or maxv text changes */
- vRangeTextChanged:sender {
    return [self _uOrVRangeTextChanged:sender varName:V];
}

/* called when one of the variables' sliders changes */
- variableSliderChanged:sender {
    int index;
    float val;
    NXEvent *event;

  /* update the associated text field */
    index = [variableSliders selectedRow];
    val = [[variableSliders selectedCell] floatValue];
    [[variableTexts cellAt:index :0] setFloatValue:val];
    [self _setVariable:index toValue:val];
    event = [NXApp currentEvent];
    [self _updateGraph:event->type == NX_LMOUSEUP];
    return self;
}

/* called when one of the variables' text fields changes */
- variableTextChanged:sender {
    int index;
    float val;
    TextFieldCell *text;
    SliderCell *slider;

  /* update the associated slider */
    text = [variableTexts selectedCell];
    index = [variableTexts selectedRow];
    slider = [variableSliders cellAt:index :0];
    val = [text floatValue];
    [self _setSliderCell:slider value:val
		minLabel:[variableMinLabels cellAt:index :0]
		maxLabel:[variableMaxLabels cellAt:index :0]];

    [self _setVariable:index toValue:val];
    [self _updateGraph:YES];
    [sender selectCell:text];
    return self;
}

/* The maximum allowable resolution.  ~10K polygons */
#define MAX_RES	100

/* called when either the resolution slider or text fields changes */
- resolutionChanged:sender {
    Cell *senderCell, *otherCell;
    int iVal;
    float fVal;
    int xyz;
    NXEvent *event;

    if ([[sender cellAt:0 :0] isKindOfClassNamed:"SliderCell"]) {
	senderCell = resolutionSlider;
	otherCell = resolutionText;
    } else {
	senderCell = resolutionText;
	otherCell = resolutionSlider;
    }

    fVal = [senderCell floatValue];
    iVal = rint(fVal);
    if (iVal > MAX_RES)
	iVal = MAX_RES;
    [otherCell setIntValue:iVal];

  /* update the Expression's resolution and display the new graph */
    for (xyz = X; xyz <= Z; xyz++)
	[expr[xyz] setResolution:iVal];
    event = [NXApp currentEvent];
    [self _updateGraph:event->type != NX_LMOUSEDRAGGED];
    if (senderCell == resolutionText)
	[sender selectText:self];
    return self;
}

/*
 * Copies the current view of the graph into the Pasteboard as Renderman code.
 * This is sent from the Copy Graph menu item.
 */
- copyGraph:sender {
    Pasteboard *pb;
    const char *types[2];
    NXDataLink *link;

    pb = [Pasteboard new];
    types[0] = N3DRIBPboardType;
    types[1] = NXDataLinkPboardType;
    [self _writeGraphToPasteboard:pb types:types num:2];
    link = [[NXDataLink alloc] initLinkedToSourceSelection:[NXSelection allSelection] managedBy:linkMgr supportingTypes:&N3DRIBPboardType count:1];
    [link writeToPasteboard:pb];
    [link free];
    return self;
}

/*
 * Copies the current view of the graph into the Pasteboard as RIB.
 * This is used by the Copy Graph menu item and to support Object Links.
 *
 * Types must contain N3DRIBPboardType.
 */
- _writeGraphToPasteboard:(Pasteboard *)pb types:(const char *const *)types num:(int)numTypes {
    NXStream *st;
    char *data;
    int dataLen, maxDataLen;

  /* Open a stream on memory where we will collect the PostScript */
    st = NXOpenMemory(NULL, 0, NX_WRITEONLY);

  /* Tell the Pasteboard we're going to copy RIB */
    [pb declareTypes:types num:numTypes owner:nil];

  /* writes the RIB for the whole graph into the stream */
    [camera copyRIBCode:st];

  /* get the buffered up PostScript out of the stream */
    NXGetMemoryBuffer(st, &data, &dataLen, &maxDataLen);

  /* put the buffer in the Pasteboard, free the stream (and the buffer) */
    [pb writeType:N3DRIBPboardType data:data length:dataLen];
    NXCloseMemory(st, NX_FREEBUFFER);
    return self;
}

/*** Object Links support - methods called by the DataLinkManager ***/

/* called by the DataLinkManager when new data is needed for a link */
- copyToPasteboard:(Pasteboard *)pboard at:(NXSelection *)selection cheapCopyAllowed:(BOOL)flag {
    NX_ASSERT([selection isEqual:[NXSelection allSelection]] || [selection isEqual:[NXSelection currentSelection]], "Funny selection passed to copyToPasteboard:at:");
    [self _writeGraphToPasteboard:pboard types:&N3DRIBPboardType num:1];
    return self;
}

/* returns the window for the given selection */
- windowForSelection:(NXSelection *)selection {
    return window;		/* all our sels are always in our one window */
}

/*
 * We support continuously updating links by tracking changes to links to
 * us from open documents.  This is easy for Graph because all links can
 * only be to the whole graph, so any change in the graph is a change relevant
 * to any link.
 */
- (BOOL)dataLinkManagerTracksLinksIndividually:(NXDataLinkManager *)sender {
    return YES;
}

/*
 * Sent when we should start tracking a link.  We just keep all links we're
 * tracking in a list, and send them a message whenever the graph is changed.
 */
- dataLinkManager:(NXDataLinkManager *)sender startTrackingLink:(NXDataLink *)link {
    if (!linksTracked)
	linksTracked = [[List allocFromZone:[self zone]] init];
    [linksTracked addObject:link];
    return self;
}

/* Sent when we can forget a links we're tracking */
- dataLinkManager:(NXDataLinkManager *)sender stopTrackingLink:(NXDataLink *)link {
    [linksTracked removeObject:link];
    if ([linksTracked count] == 0) {
	[linksTracked free];
	linksTracked = nil;
    }
    return self;
}

/*
 * Called by other methods of Graph3DDoc whenever we make a change to the
 * document.  We tell the DataLinkManager about the change, and also notify
 * any links we are tracking.
 */
- docChanged {
    [linkMgr documentEdited];
    [linksTracked makeObjectsPerform:@selector(sourceEdited)];
    [window setDocEdited:YES];
    return self;
}

/* called by ThreeDPanel when it changes something about the document */
- threeDPanelDidChangeDoc:(ThreeDPanel *)sender {
    [self docChanged];
    return self;
}


/* called when Save, Save As, or Save To is picked from the menu */
- save:sender   {  return [self _doSaveWithNewName:NO retainName:YES];  }
- saveAs:sender {  return [self _doSaveWithNewName:YES retainName:YES]; }
- saveTo:sender {  return [self _doSaveWithNewName:YES retainName:NO];  }

/*
 * All varieties of save go through this routine.  It covers all the cases
 * of running the Save Panel and retaining the name chosen.
 */
- _doSaveWithNewName:(BOOL)doNewName retainName:(BOOL)doRetain {
    SavePanel *savePanel;
    const char *saveName;	/* filename to save into */
    NXZone *zone;
    BOOL previouslySaved = (name != NULL);
    char realPath[MAXPATHLEN+1];

  /*
   * If the file is untitled or we are saving under a different name,
   * run the save panel to get a new name.
   */
    zone = [self zone];
    if (!name || doNewName) {
	savePanel = [SavePanel new];
	[savePanel setRequiredFileType:"xyzgraph"];
	if ([savePanel runModalForDirectory:NULL file:name]) {
	  /* if we want to keep this name, replace any old name */	
	    if (doRetain) {
		NXZoneFree(zone, name);
		NXZoneFree(zone, realName);
		realName = NULL;
		name = NXCopyStringBufferFromZone([savePanel filename], zone);
	    }
	    saveName = [savePanel filename];
	} else
	    return nil;		/* user canceled */
    } else
      /* if we didn't run the Save Panel, save using the existing name */
	saveName = name;
    [self _write:saveName];
    if (!doRetain)
	[linkMgr documentSavedTo:saveName];
    else if (!previouslySaved || doNewName)
	[linkMgr documentSavedAs:saveName];
    else
	[linkMgr documentSaved];
    if (doRetain) {
	[window setDocEdited:NO];
	[window setTitleAsFilename:name];
	if (!realName && realpath(name, realPath)) {
	    realName = NXCopyStringBufferFromZone(realPath, [self zone]);
	}
    }
    return self;
}

- revertToSaved:sender {
    int response;
    NXStringTable *stringTable;
    const char *shortName;
    Graph3DDoc *newDoc;

    if ([window isDocEdited] && name) {
	stringTable = [[NXApp delegate] stringTable];
	shortName = strrchr(name, '/') + 1;
	response =  NXRunAlertPanel([stringTable valueForStringKey:"revert alert title"],
			[stringTable valueForStringKey:"revert alert message"],
			[stringTable valueForStringKey:"revert button"],
			[stringTable valueForStringKey:"cancel button"],
			NULL, shortName);
	if (response == NX_ALERTDEFAULT) {
	    [window setDelegate:nil];
	    [window close];
	    [NXApp delayedFree:self];
	    [linkMgr documentClosed];
	    [linkMgr free];
	    linkMgr = nil;
	    newDoc = [[Graph3DDoc allocFromZone:[self zone]] initFromFile:name];
	    if (!newDoc) {
		NXRunAlertPanel(
		    [stringTable valueForStringKey:"open alert title"],
		    [stringTable valueForStringKey:"open alert message"],
		    [stringTable valueForStringKey:"ok button"],
		    NULL, NULL, name);
	    }
	}
    }
    return self;
}

/* Called when the window is closing.  We free the Graph3DDoc. */
- windowWillClose:sender {
    int response;
    NXStringTable *stringTable;
    const char *shortName;

    if ([window isDocEdited]) {
	stringTable = [[NXApp delegate] stringTable];
	if (name) {
	    shortName = strrchr(name, '/') + 1;
	} else {
	    shortName = [stringTable valueForStringKey:"untitled doc"];
	}
	response =  NXRunAlertPanel([stringTable valueForStringKey:"close alert title"],
			[stringTable valueForStringKey:"close alert message"],
			[stringTable valueForStringKey:"save button"],
			[stringTable valueForStringKey:"dont save button"],
			[stringTable valueForStringKey:"cancel button"],
			shortName);
	if (response != NX_ALERTDEFAULT && response != NX_ALERTALTERNATE) {
	    return nil;
	} else {
	    if (response == NX_ALERTDEFAULT && ![self save:sender])
		return nil;
	}
    }
    [NXApp delayedFree:self];
    [linkMgr documentClosed];
    return self;			/* says its OK to close */
}

/* Called whenever something changes to update the view of the graph. */
- _updateGraph:(BOOL)finalChange {
    [self _updateShape];	/* update the values that we graph */
    [camera display];		/* draw the new graph */
    if (finalChange)
	[self docChanged];
    return self;
}

/* Called whenever something changes to update the values that we graph. */
- _updateShape {
    float *vals[3];
    int numVals[3];
    float min[3], max[3];
    int xyz;

  /* extract the values from the Expressions.  This may cause a recalc. */
    for (xyz = X; xyz <= Z; xyz++) {
	[expr[xyz] resultsVector:&vals[xyz] numVals:&numVals[xyz]];
	[expr[xyz] resultsMin:&min[xyz] max:&max[xyz]];
    }

  /* set the points of the graph */
    [[camera worldShape] setPointsX:vals[X] y:vals[Y] z:vals[Z]
			minX:min[X] minY:min[Y] minZ:min[Z]
			maxX:max[X] maxY:max[Y] maxZ:max[Z]
			numU:[expr[X] resolution] numV:[expr[X] resolution]];
    return self;
}

/* parses and validates the text for one equation field */
- (BOOL)_parseEquationFrom:(const char *)str using:(Expression *)anExpr enableVariables:(BOOL *)enableVars {
    BOOL parseError;
    EXPEnumState state;	/* used to run through the vars of the expression */
    const char *var;

    parseError = ![anExpr parse:str];
    if (!parseError) {
	/*
	* If it parsed successfully, make sure all the variables are single
	* letters, and are either "u", "v" or between "A" and "H".  As we
	* find suitable variables, we remember then in the enableVars
	* array, so we can enable their controls later.
	*/
	state = [anExpr beginVariableEnumeration];
	while (var = [anExpr nextVariable:state])
	    if (*var && !var[1] && *var >= MIN_CONST && *var <= MAX_CONST)
		enableVars[*var - MIN_CONST] = YES;
	    else if ((*var != 'u' && *var != 'v') || var[1])
		parseError = YES;
	[anExpr endVariableEnumeration:state];
    }
    return parseError;
}

/*
 * Writes a document to the given file using typedstreams.  We're very careful
 * about catching exceptions here so we can tell the user if his file didn't
 * get written out successfully.
 */
- _write:(const char *)filename {
    NXTypedStream *ts;
    const char *stringVal;
    int intVal;
    char charVal;
    RtPoint fromPoint;
    RtPoint toPoint;
    float aRollAngle;
    RtMatrix shapeXform;
    int rows, cols;
    int numVars;
    int i;
    NXStringTable *stringTable;
    volatile BOOL hadAnError = NO;
    char buffer[MAXPATHLEN+1];
    int xyz;
    NXRect winFrame;

  /* cons up the name of the backup file and remove it */
    strcpy(buffer, filename);
    strcat(buffer, "~");
    removeFile(buffer);

  /* move the existing file to the backup */
    rename(filename, buffer);

  /* create the new document as a file package (doc wrapper) */
    strcpy(buffer, filename);
    strcat(buffer, DOC_NAME);
    mkdir(filename, 0777);
    ts = NXOpenTypedStreamForFile(buffer, NX_WRITEONLY);
    if (ts) {
	NX_DURING
	    intVal = 2;			/* version of this write's data */
	    NXWriteType(ts, "i", &intVal);
	    for (xyz = X; xyz <= Z; xyz++) {
		stringVal = [expr[xyz] text];
		NXWriteType(ts, "*", &stringVal);
	    }
	    [variableSliders getNumRows:&rows numCols:&cols];
	    numVars = 0;
	    for (i = 0; i < rows; i++) {
		if ([[variableSliders cellAt:i :0] isEnabled])
		    numVars++;
	    }
	    NXWriteType(ts, "i", &numVars);
	    for (i = 0; i < rows; i++)
		if ([[variableSliders cellAt:i :0] isEnabled]) {
		    charVal = MIN_CONST + i;
		    NXWriteType(ts, "c", &charVal);
		    writeCellFloat([variableSliders cellAt:i :0], ts);
		}
	    intVal = [expr[X] resolution];
	    NXWriteType(ts, "i", &intVal);
	    writeCellFloat(minUVSlider[U], ts);
	    writeCellFloat(maxUVSlider[U], ts);
	    writeCellFloat(minUVSlider[V], ts);
	    writeCellFloat(maxUVSlider[V], ts);
	    NXWriteColor(ts, [[camera worldShape] color]);
	    NXWriteColor(ts, [camera backgroundColor]);
	    intVal = [[camera worldShape] surfaceType];
	    NXWriteType(ts, "i", &intVal);
	    [camera getEyeAt:&fromPoint toward:&toPoint roll:&aRollAngle];
	    NXWriteArray(ts, "f", 3, fromPoint);
	    NXWriteArray(ts, "f", 3, toPoint);
	    NXWriteType(ts, "f", &aRollAngle);
	    [[camera worldShape] getTransformMatrix:shapeXform];
	    NXWriteArray(ts, "f", 4, shapeXform[0]);
	    NXWriteArray(ts, "f", 4, shapeXform[1]);
	    NXWriteArray(ts, "f", 4, shapeXform[2]);
	    NXWriteArray(ts, "f", 4, shapeXform[3]);
	    [window getFrame:&winFrame];
	    NXWriteRect(ts, &winFrame);
	    NXCloseTypedStream(ts);
	NX_HANDLER
	    hadAnError = YES;
	    NX_DURING
		NXCloseTypedStream(ts);
	    NX_HANDLER
		/* ignore any error at this point */
	    NX_ENDHANDLER
	NX_ENDHANDLER
    } else 
	hadAnError = YES;
    if (hadAnError) {
	stringTable = [[NXApp delegate] stringTable];
	NXRunAlertPanel([stringTable valueForStringKey:"save alert title"],
			[stringTable valueForStringKey:"save alert message"],
			[stringTable valueForStringKey:"ok button"],
			NULL, NULL, filename);
	return nil;
    } else
	return self;
}

/* reads a document from the given file using typedstreams */
- _read:(const char *)filename {
    NXTypedStream *ts;
    char *stringVal;
    int i;
    char varName;
    float floatVal1, floatVal2;
    int intVal;
    RtPoint fromPoint;
    RtPoint toPoint;
    float aRollAngle;
    RtMatrix shapeXform;
    int version;
    int numVars;
    BOOL parseError;
    int xyz, uv;
    NXRect winFrame;

    ts = openDocStream(filename, NX_READONLY);
    if (ts) {
	NX_DURING
	    NXReadType(ts, "i", &version);	/* read file version */
	    NX_ASSERT(version <= 2, "Unknown file version in -read");
	    for (xyz = X; xyz <= Z; xyz++) {
		NXReadType(ts, "*", &stringVal);
		[equation[xyz] setStringValue:stringVal];
		parseError = ![expr[xyz] parse:stringVal];
		free(stringVal);
		NX_ASSERT(!parseError, "Bad expression stored in data file");
		if (parseError)
		    NX_VALRETURN(nil);
	    }
	    NXReadType(ts, "i", &numVars);
	    for (i = 0; i < numVars; i++) {
		NXReadType(ts, "c", &varName);
		NXReadType(ts, "f", &floatVal1);
		[self _initVariable:varName - MIN_CONST enable:YES
							value:floatVal1];
	    }
	    NXReadType(ts, "i", &intVal);		/* read resolution */
	    [resolutionText setIntValue:intVal];
	    [resolutionSlider setIntValue:intVal];
	    for (xyz = X; xyz <= Z; xyz++)
		[expr[xyz] setResolution:intVal];
	    for (uv = U; uv <= V; uv++) {
		NXReadType(ts, "f", &floatVal1);	/* read min */
		NXReadType(ts, "f", &floatVal2);	/* read max */
		[minUVText[uv] setFloatValue:floatVal1];
		[self _setSliderCell:minUVSlider[uv] value:floatVal1
						minLabel:nil maxLabel:nil];
		[maxUVText[uv] setFloatValue:floatVal2];
		[self _setSliderCell:maxUVSlider[uv] value:floatVal2
						minLabel:nil maxLabel:nil];
		[self _setUVRangeOf:uv];
	    }
	    [[camera worldShape] setColor:NXReadColor(ts)];
	    [camera setBackgroundColor:NXReadColor(ts)];
	    NXReadType(ts, "i", &intVal);		/* read surface type */
	    [camera setSurfaceTypeForAll:intVal chooseHider:YES];
	    NXReadArray(ts, "f", 3, fromPoint);
	    NXReadArray(ts, "f", 3, toPoint);
	    NXReadType(ts, "f", &aRollAngle);
	    [camera setEyeAt:fromPoint toward:toPoint roll:aRollAngle];
	    NXReadArray(ts, "f", 4, shapeXform[0]);
	    NXReadArray(ts, "f", 4, shapeXform[1]);
	    NXReadArray(ts, "f", 4, shapeXform[2]);
	    NXReadArray(ts, "f", 4, shapeXform[3]);
	    [[camera worldShape] setTransformMatrix:shapeXform];
	    NXReadRect(ts, &winFrame);
	    [window placeWindow:&winFrame];
	    NXCloseTypedStream(ts);
	  /* must use NX_VALRETURN to return from inside a NX_DURING */
	    NX_VALRETURN(self);
	NX_HANDLER
	    NX_DURING
		NXCloseTypedStream(ts);
	    NX_HANDLER
		/* ignore any error at this point */
	    NX_ENDHANDLER
	    return nil;
	NX_ENDHANDLER
    } else
	return nil;
}

/*
 * Inits a variable to either be on or off.  Used when we discover a new
 * set of variables after a parse, or to set up the variables from a document
 * that we read from a file.
 */
- (void)_initVariable:(int)index enable:(BOOL)flag value:(float)val {
    TextFieldCell *text;
    SliderCell *slider;

  /* set the initial value in the Expression */
    if (flag)
	[self _setVariable:index toValue:val];

  /* if the enabled status is changing... */
    if (flag != [[variableSliders cellAt:index :0] isEnabled])
	if (flag) {

	  /* enable all controls associated with the variable */
	    [[variableLabels cellAt:index :0] setTextGray:NX_BLACK];
	    slider = [variableSliders cellAt:index :0];
	    [slider setEnabled:YES];
	    [self _setSliderCell:slider value:val
			minLabel:[variableMinLabels cellAt:index :0]
			maxLabel:[variableMaxLabels cellAt:index :0]];
	    text = [variableTexts cellAt:index :0];
	    [text setEnabled:YES];
	    [text setFloatValue:val];
	} else if (!flag) {

	  /* disable all controls associated with the variable */
	    [[variableLabels cellAt:index :0] setTextGray:NX_DKGRAY];
	    slider = [variableSliders cellAt:index :0];
	    [slider setFloatValue:[slider minValue]];
	    [slider setEnabled:NO];
	    text = [variableTexts cellAt:index :0];
	    [text setStringValue:NULL];
	    [text setEnabled:NO];
	}
}

/*
 * sets the range of the u or v variable in the Expression.
 * It ensures that the min value isn't greater than the max value.
 */
- (void)_setUVRangeOf:(int)var {
    int xyz;
    float min = [minUVSlider[var] floatValue];
    float max = [maxUVSlider[var] floatValue];
    char *varName = (var == U) ? "u" : "v";

    for (xyz = X; xyz <= Z; xyz++) {
	[expr[xyz] setVar:varName min:MIN(min, max) max:MAX(min, max)];
      /* set up two dimensions of evaluation */
	[expr[xyz] setVar:varName dimension:var];
    }
}

/* sets one of the expressions' variables to a value */
- (void)_setVariable:(int)index toValue:(float)val {
    char varName[2];
    int xyz;

    varName[0] = MIN_CONST + index;
    varName[1] = '\0';
    for (xyz = X; xyz <= Z; xyz++)
	[expr[xyz] setVar:varName value:val];
}

/* sets a slider to a value and adjust min/max */
- (void)_setSliderCell:(SliderCell *)slider value:(float)val minLabel:(Cell *)minLabel maxLabel:(Cell *)maxLabel {
    if (val < [slider minValue]) {
	[window disableDisplay];   /* so slider won't flash to new location */
	[slider setMinValue:val];
	[window reenableDisplay];
	[minLabel setIntValue:floor(val)];
    } else if (val > [slider maxValue]) {
	[window disableDisplay];   /* so slider won't flash to new location */
	[slider setMaxValue:val];
	[window reenableDisplay];
	[maxLabel setIntValue:ceil(val)];
    }
    [slider setFloatValue:val];
}

/* sets up some default lights for the camera */
- (void)_setUpLighting {
    RtPoint distantlight_from, distantlight2_from, pointlight_from;
    RtPoint distantlight_to;
    RtFloat intensity;
    N3DLight *pointLight, *distantLight, *distantLight2, *ambientLight;

    if ([[camera lightList] count] == 4)
	return;		/* we've already added lights to this camera */

    pointlight_from[0] = 12.0;
    pointlight_from[1] = -7.0;
    pointlight_from[2] = 8.5;
    distantlight_from[0] = 12.0;
    distantlight_from[1] = -7.0;
    distantlight_from[2] = 8.5;
    distantlight2_from[0] = 3.0;
    distantlight2_from[1] = 2.0;
    distantlight2_from[2] = 8.5;

    distantlight_to[0] = 0.0;
    distantlight_to[1] = 0.0;
    distantlight_to[2] = 0.0;

    intensity = 60.0;
    pointLight = [[N3DLight allocFromZone:[camera zone]] init];
    [pointLight makePointFrom:pointlight_from intensity:intensity];
    [camera addLight:pointLight];

    intensity = 0.6;
    distantLight = [[N3DLight allocFromZone:[camera zone]] init];
    [distantLight makeDistantFrom:distantlight_from to:distantlight_to intensity:intensity];
    [camera addLight:distantLight];

    intensity = 0.9;
    distantLight2 = [[N3DLight allocFromZone:[camera zone]] init];
    [distantLight2 makeDistantFrom:distantlight2_from to:distantlight_to intensity:intensity];
    [camera addLight:distantLight2];

    intensity = 0.15;
    ambientLight = [[N3DLight allocFromZone:[camera zone]] init];
    [ambientLight makeAmbientWithIntensity:intensity];
    [camera addLight:ambientLight];
}

/*
 * These methods are called as the IB file is unarchived.  We override them to
 * store these outlets in arrays, since IB doesnt know how to declare arrays
 * of outlets.
 */
- setEquationX:obj	{ equation[X] = obj; return self;	}
- setEquationY:obj	{ equation[Y] = obj; return self;	}
- setEquationZ:obj	{ equation[Z] = obj; return self;	}
- setMinUSlider:obj 	{ minUVSlider[U] = obj; return self;	}
- setMinVSlider:obj	{ minUVSlider[V] = obj; return self;	}
- setMaxUSlider:obj	{ maxUVSlider[U] = obj; return self;	}
- setMaxVSlider:obj	{ maxUVSlider[V] = obj; return self;	}
- setMinUText:obj	{ minUVText[U] = obj; return self;	}
- setMinVText:obj	{ minUVText[V] = obj; return self;	}
- setMaxUText:obj	{ maxUVText[U] = obj; return self;	}
- setMaxVText:obj	{ maxUVText[V] = obj; return self;	}

@end

/* little utility proc to write out the float value of a control */
static void writeCellFloat(Cell *obj, NXTypedStream *ts) {
    float val;

    val = [obj floatValue];
    NXWriteType(ts, "f", &val);
}

/*  Opens a stream on the document regardless of whether its a doc wrapper. */
static NXTypedStream *openDocStream(const char *filename, int mode) {
    NXTypedStream *ts = NULL;
    struct stat statInfo;
    char buffer[MAXPATHLEN+1];

    if (stat(filename, &statInfo) == 0) {
	if (statInfo.st_mode & S_IFDIR) {
	    strcpy(buffer, filename);
	    strcat(buffer, DOC_NAME);
	    ts = NXOpenTypedStreamForFile(buffer, mode);
	} else {
	    ts = NXOpenTypedStreamForFile(filename, mode);
	}
    }
    return ts;
}

/* removes a directory, removing anything inside it.  Does not recurse */
static int removeFile(const char *file) {
    DIR *dirp;
    struct stat st;
    struct direct *dp;
    char *leaf = NULL;
    char path[MAXPATHLEN+1];

    if (!stat(file, &st)) {
	if ((st.st_mode & S_IFMT) == S_IFDIR) {
	    dirp = opendir(file);
	    for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp)) {
		if (strcmp(dp->d_name, ".") && strcmp(dp->d_name, "..")) {
		    if (!leaf) {
			strcpy(path, file);
			strcat(path, "/");
			leaf = path + strlen(path);
		    }
		    strcpy(leaf, dp->d_name);
		    if (unlink(path)) {
			closedir(dirp);
			return -1;
		    }
		}
	    }
	    return rmdir(file);
	} else {
	    return unlink(file);
	}
    }

    return -1;
}
